package com.toncen.samepms.common;

import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.ServletActionContext;

import com.toncen.samepms.basic.domain.User;
import com.toncen.samepms.job.IntelligentAdviceJob;
import com.toncen.samepms.job.OperatingLogJob;
import com.toncen.samepms.system.dao.AdviceDao;
import com.toncen.samepms.system.domain.Advice;
import com.toncen.samepms.system.domain.OperatingLog;
import com.ibatis.common.resources.Resources;
import com.ibatis.sqlmap.client.SqlMapClient;
import com.ibatis.sqlmap.client.SqlMapClientBuilder;
import com.sky.common.annotation.AnnotationUtils;
import com.sky.common.http.HTTPUtils;
import com.sky.common.json.JSONUtils;
import com.sky.common.lang.ClassUtils;
import com.sky.common.string.StringUtils;
import com.sky.fisher.service.FisherService;

/**
 * @author tch
 * 
 */
public abstract class AbstractDao<M extends IDomain> implements IDao<M> {
	/**
	 * 新增statement的后缀
	 */
	public static final String INSERT_POSTFIX = "add";

	/**
	 * 删除statement的后缀
	 */
	public static final String DELETE_POSTFIX = "delById";

	/**
	 * 修改statement的后缀
	 */
	public static final String UPDATE_POSTFIX = "update";

	/**
	 * 查询statement的后缀
	 */
	public static final String QUERY_POSTFIX = "getById";

	/**
	 * 默认Ibatis配置文件
	 */
	public static final String DEFAULT_CONFIGURATION_FILE = "SqlMapConfig.xml";

	/**
	 * 
	 */
	private static Hashtable<String, SqlMapClient> CACHE_SQLMAPCLIENTS = new Hashtable<String, SqlMapClient>();

	protected Log logger = LogFactory.getLog(this.getClass());

	/**
	 * 
	 */
	private Class domainClass;

	/**
	 * IBatis客户端实例
	 */
	private SqlMapClient SqlMap;

	/**
	 * IBatis配置文件，默认为SqlMapConfig.xml
	 */
	private String configFile = DEFAULT_CONFIGURATION_FILE;

	/**
	 * statement的前缀
	 */
	private String statementPrefix = "";

	/**
	 * statement的分割符，默认为英文下划线"_"
	 */
	private String statementDivision = "_";

	/**
	 * 构造函数，默认使用SqlMapConfig.xml文件。
	 * 
	 */
	public AbstractDao() {
		this(DEFAULT_CONFIGURATION_FILE);
	}

	/**
	 * 构造函数，可以在子类中指定配置文件。
	 * 
	 */
	public AbstractDao(String configFile) {
		domainClass = ClassUtils.getParameterizedType(this.getClass(), 0);
		this.statementPrefix = domainClass.getSimpleName();
		this.configFile = configFile;
		initialize();// 初始化
	}

	/**
	 * @return the domainClass
	 */
	public Class getDomainClass() {
		return domainClass;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.toncen.samepms.common.IDao#add(M)
	 */
	public long add(M domain) throws Exception {
		if (!domain.isSaved()) {// 自动设置当前主键
			domain.setId(getPrimaryID());
		}
		notifyListener(domain, Action.ADD);
		Object tempObj = getSqlMap().insert(getStatement(INSERT_POSTFIX), domain);
		long key = domain.getId();// 默认取自己设置的主键
		if (tempObj != null) {// 配置了自动生成规则则返回自动生成的主键
			key = Long.parseLong(tempObj.toString());
		} else if (!domain.isSaved()) {// 没有配置自动生成也没有指定主键，则抛出错误
			throw new Exception("【" + domain.getClass() + "】没有配置主键生成规则，请配置...");
		}
		return key;
	}

	/**
	 * 批量新增
	 * 
	 * @param domains
	 * @throws Exception
	 */
	public void add(Collection<M> domains) throws Exception {
		for (M domain : domains) {
			add(domain);
		}
	}

	private static long PrimaryID = 0;

	/**
	 * 主键锁
	 */
	private static final Object PRIMARY_LOCK = new Object();

	/**
	 * 获取主键
	 * 
	 * @return
	 * @throws Exception
	 */
	public synchronized static long getPrimaryID() throws Exception {
		synchronized (PRIMARY_LOCK) {
			long id = 0;
			do {
				id = Calendar.getInstance().getTimeInMillis();
			} while (id == PrimaryID);
			PrimaryID = id;
			return id;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.toncen.samepms.common.IDao#update(M)
	 */
	public int update(M domain) throws Exception {
		notifyListener(domain, Action.UPDATE);
		return getSqlMap().update(getStatement(UPDATE_POSTFIX), domain);
	}

	/**
	 * 批量更新
	 * 
	 * @param domains
	 * @throws Exception
	 */
	public void update(Collection<M> domains) throws Exception {
		for (M domain : domains) {
			update(domain);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.toncen.samepms.common.IDao#load(long)
	 */
	public M load(long id) throws Exception {
		M domain = this.find(id);
		if (domain == null) {
			throw new Exception("你要访问的资源已经被其他用户占用，请稍后再试...");
		}
		return domain;
	}

	/**
	 * 根据对象加载对象,配置文件statement的id命名规则：statementPrefix(表名) +statementDivision(分隔符)
	 * +getById(后缀)
	 * 
	 * @param domain
	 * @return 返回加载的对象，没有找到对象则抛出Exception异常
	 * @throws Exception
	 */
	public M load(M domain) throws Exception {
		return load(domain.getId());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.toncen.samepms.common.IDao#find(long)
	 */
	@SuppressWarnings("unchecked")
	public M find(long id) throws Exception {
		return (M) getSqlMap().queryForObject(getStatement(QUERY_POSTFIX), id);
	}

	/**
	 * 根据id查询对象,配置文件statement的id命名规则：statementPrefix(表名) +statementDivision(分隔符)
	 * +getById(后缀)
	 * 
	 * @param domain
	 * @return 如果找到则返回此对象，没有找到则返回空引用null
	 * @throws Exception
	 */
	public M find(M domain) throws Exception {
		if (domain == null) {
			return null;
		}
		return this.find(domain.getId());
	}

	/**
	 * 根据指定的statement的后缀statementPostfix查询对象list,配置文件statement的id命名规则：statementPrefix(表名)
	 * +statementDivision(分隔符) +statementPostfix(后缀)
	 * 
	 * @param statementPostfix
	 *            statement的id的后缀
	 * @param parameter
	 *            当前查询的参数
	 * @return 查询到的结果list，没有找到则返回空list
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	public List<M> findList(String statementPostfix, Object parameter) throws Exception {
		List<M> resultList = getSqlMap().queryForList(getStatement(statementPostfix), parameter);
		if (resultList == null) {
			resultList = new ArrayList<M>(5);
		}
		return resultList;
	}

	/**
	 * @param statementPostfix
	 * @param parameter
	 * @return
	 * @throws Exception
	 */
	public List queryForList(String statementPostfix, Object parameter) throws Exception {
		List resultList = getSqlMap().queryForList(getStatement(statementPostfix), parameter);
		if (resultList == null) {
			resultList = new ArrayList(5);
		}
		return resultList;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.toncen.samepms.common.IDao#save(com.toncen.samepms.common.IDomain)
	 */
	public long save(M domain) throws Exception {
		if (domain.isSaved() && find(domain) != null) {// 已经持久化，则修改
			return update(domain);
		} else {// 新增
			return add(domain);
		}
	}

	/**
	 * 批量保存
	 * 
	 * @param domains
	 * @throws Exception
	 */
	public void save(Collection<M> domains) throws Exception {
		for (M domain : domains) {
			save(domain);
		}
	}

	/**
	 * 批量对比保存列表
	 * 
	 * @param oldDomains
	 *            原数据库中的列表
	 * @param newDomains
	 *            修改后的列表
	 * @throws Exception
	 */
	public void save(Collection<M> oldDomains, Collection<M> newDomains) throws Exception {
		for (M domain : oldDomains) {
			if (!newDomains.contains(domain)) {// 数据库中有，当前实际的没有，则删除数据库中的
				this.delete(domain);
			}
		}
		for (M domain : newDomains) {// 当前实际的全部保存
			this.save(domain);
		}
	}

	/**
	 * 根据对象删除对象,配置文件statement的id命名规则：statementPrefix(表名) +statementDivision(分隔符)
	 * +delById(后缀)
	 * 
	 * @param domain
	 * @return 受影响的记录数目
	 * @throws Exception
	 */
	public int delete(M domain) throws Exception {
		return delete(domain.getId());
	}

	/**
	 * 根据id数组批量删除对象(每个元素委托根据id删除对象方法，具体配置也和根据id删除对象方法相同)
	 * 
	 * @param domainIds
	 * @return 受影响的记录数目
	 * @throws Exception
	 */
	public int delete(long[] domainIds) throws Exception {
		int total = 0;
		for (long id : domainIds) {
			total += delete(id);
		}
		return total;
	}

	/**
	 * 根据对象集合批量删除对象(每个集合元素委托根据对象删除对象方法，具体配置也和根据对象删除对象方法相同)
	 * 
	 * @param domainCollection
	 * @return 受影响的记录数目
	 * @throws Exception
	 */
	public int delete(Collection<M> domainCollection) throws Exception {
		int total = 0;
		for (M domain : domainCollection) {
			total += delete(domain);
		}
		return total;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.toncen.samepms.common.IDao#delete(long)
	 */
	@SuppressWarnings("unchecked")
	public int delete(long id) throws Exception {
		M domain = (M) ClassUtils.newInstance(getDomainClass().getName());
		domain.setId(id);
		notifyListener(domain, Action.DELETE);
		return getSqlMap().delete(getStatement(DELETE_POSTFIX), id);
	}

	/**
	 * 清除当前缓存
	 */
	public void clearCache() {
		try {
			String cacheModel = getStatement(getDomainClass().getSimpleName() + "Cache");
			getSqlMap().flushDataCache(cacheModel);
		} catch (Exception e) {
		}
	}

	/**
	 * 清除全部缓存
	 */
	public void clearAllCache() {
		getSqlMap().flushDataCache();
	}

	/**
	 * 通知监听器
	 * 
	 * @param domain
	 * @param action
	 * @throws Exception
	 */
	private void notifyListener(M domain, Action action) throws Exception {
		try {
			notifyAllListener(domain, action);
		} catch (ListenerException le) {// 监听异常，则抛出
			throw le;
		} catch (Exception oe) {// 其它异常则忽略
			oe.printStackTrace();
			logger.error(oe.getMessage(), oe);
		}
	}

	/**
	 * 通过所有监听器
	 * 
	 * @param domain
	 * @param action
	 */
	protected void notifyAllListener(M domain, Action action) throws Exception {
		logger.debug("Domain【" + domain.getClass().getName() + "(" + domain.getId() + ")】执行【" + action.getName() + "】...");
		if (action != Action.DELETE && domain instanceof IPropertyUniqueble) {// 属性唯一验证
			try {
				domainPropertyUnique(domain);
			} catch (Exception e) {
				throw new ListenerException(e.getMessage(), e);
			}
		}
		if (action != Action.DELETE && domain instanceof IAdviceable) {// 智能通知
			String lastState = getObjectState(domain, (IAdviceable) domain);
			AdviceDao adviceDao = new AdviceDao();
			List<Advice> advices = adviceDao.getByState(1);
			for (Advice advice : advices) {
				if (ComitConstant.isDeveloperMode()) {// 开发模式不使用计划任务发布
					advice.publish(domain, this, lastState);
				} else {
					IntelligentAdviceJob.addIntelligentAdviceContext(advice, domain, lastState, this);
				}
			}
		}
		writeOperation(domain, action);// 保存操作日志
	}

	/**
	 * TODO 属性唯一验证
	 * 
	 * @author Jun 15, 2012 8:06:48 PM ZQ
	 * @param domain
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws SQLException
	 * @throws Exception
	 * 
	 */
	@SuppressWarnings("unchecked")
	private void domainPropertyUnique(M domain) throws Exception {
		// 属性验证
		IPropertyUniqueble propertyUniqueble = (IPropertyUniqueble) domain;
		List<Field> uniqueFields = new ArrayList<Field>();// 属性上配置唯一验证
		AnnotationUtils.addAllFields(Unique.class, domain.getClass(), uniqueFields);
		if (uniqueFields.size() == 0) {
			throw new Exception("没有配置【" + Unique.class.getName() + "】属性!");
		}

		// 传进ibatis的map参数
		Map<String, Object> paraMap = new HashMap<String, Object>();
		// 临时map
		Map<String, Object> selectMap = null;
		Map<String, Object> whereMap = null;
		// select 字段
		List<Map<String, Object>> selectFieldList = new ArrayList<Map<String, Object>>();
		// where 条件
		List<Map<String, Object>> whereArgList = new ArrayList<Map<String, Object>>();
		// 表名
		paraMap.put("selectTable", propertyUniqueble.getTableName().toUpperCase());
		// 排除本记录
		paraMap.put("notEqualeKey", propertyUniqueble.getPrimaryName().toUpperCase());
		paraMap.put("notEqualeValue", domain.getId());
		Unique unique;
		String column;
		String condition = "and";//条件
		// 属性where 语句
		Object value = null;
		// 属性 select 语句
		for (Field field : uniqueFields) {
			unique = field.getAnnotation(Unique.class);
			column = unique.column();
			if (unique.complexField() || unique.ignoreField()) {
				value = PropertyUtils.getProperty(domain, field.getName());// 先获取复杂对象
				if (value != null) {// 复杂对象存在
					// 复杂属性，路过
					value = PropertyUtils.getProperty(domain, field.getName() + ".id");
				}
			} else {
				value = PropertyUtils.getProperty(domain, field.getName());
			}
			if (!StringUtils.isEmpty(value)) {
				whereMap = new HashMap<String, Object>();
				whereMap.put("key", column.toUpperCase());
				whereMap.put("value", value);
				whereMap.put("condition", unique.condition().toUpperCase());
				condition = unique.condition().toUpperCase();
				whereArgList.add(whereMap);

				selectMap = new HashMap<String, Object>();
				selectMap.put("key", column + " as " + column.toUpperCase());
				selectFieldList.add(selectMap);
			}
		}
		if (selectFieldList.size() == 0 || whereArgList.size() == 0) {// 无唯一性验证属性则返回
			return;
		}
		paraMap.put("selectFieldList", selectFieldList);
		paraMap.put("whereArgList", whereArgList);
		// 查询
		List<Map<String, Object>> results = new ArrayList<Map<String,Object>>();
		if(condition.toLowerCase().equals("and"))
		{
			results = getUniqueDomain(paraMap);
		}
		else
		{
			results = getUniqueBySqlConditionOr(paraMap);
		}
		if (results.size() > 0) {
			Map<Unique, Object> errors = new HashMap<Unique, Object>();
			// 弹出错误信息
			StringBuffer errorMsg = new StringBuffer();
			// 遍历结果集
			for (Map<String, Object> result : results) {
				// 遍历annotation
				for (Field field : uniqueFields) {
					unique = field.getAnnotation(Unique.class);
					column = unique.column();
					if (unique.complexField() || unique.ignoreField()) {
						// 复杂属性，路过
						continue;
					}
					if (!errors.containsKey(unique) && !StringUtils.isEmpty(result.get(column.toUpperCase()))) {
						errors.put(unique, result.get(column.toUpperCase()));
					}
				}
			}
			for (Unique u : errors.keySet()) {
				errorMsg.append("、");
				errorMsg.append("【");
				errorMsg.append(u.title());
				errorMsg.append("】");
				errorMsg.append("(");
				errorMsg.append(errors.get(u));
				errorMsg.append(")");
			}
			if (errors.size() > 0) {
				// 抛出异常
				throw new Exception("与"+errorMsg.toString().replaceFirst("、", "") + "重复，请重新输入。");
			}
		}
	}

	/**
	 * 获取通知业务对象修改之前的状态
	 * 
	 * @param domain
	 * @param advice
	 * @throws Exception
	 */
	private String getObjectState(M domain, IAdviceable advice) throws Exception {
		StringBuffer strSQL = new StringBuffer();
		String tableName = advice.getTableName();
		if (StringUtils.isEmpty(tableName)) {
			throw new Exception("通知[" + advice.getClass().getName() + "]表名没有设置，请与系统管理员联系！");
		}
		String primaryName = advice.getPrimaryName();
		if (StringUtils.isEmpty(advice.getPrimaryName())) {
			primaryName = "ID";
			logger.debug("通知[" + advice.getClass().getName() + "]主键字段名没有设置，系统将使用默认主键字段名[ID]...");
		}
		String stateName = advice.getStateName();
		if (StringUtils.isEmpty(advice.getStateName())) {
			stateName = "STATE";
			logger.debug("通知[" + advice.getClass().getName() + "]状态字段名没有设置，系统将使用默认状态字段名[STATE]...");
		}
		strSQL.append(" SELECT ");
		strSQL.append(" ");
		strSQL.append(stateName);
		strSQL.append(" ");
		strSQL.append(" FROM ");
		strSQL.append(" ");
		strSQL.append(tableName);
		strSQL.append(" ");
		strSQL.append(" WHERE ");
		strSQL.append(" ");
		strSQL.append(primaryName);
		strSQL.append(" = ");
		strSQL.append(" ");
		strSQL.append(domain.getId());
		strSQL.append(" ");
		List<Map<String, String>> resultList = FisherService.getConnectionProvider(null).getQuery().executeQuery(strSQL.toString());
		if (!CollectionUtils.isEmpty(resultList) && !StringUtils.isEmpty(resultList.get(0).get(stateName))) {
			return resultList.get(0).get(stateName);
		} else {
			return null;
		}
	}

	/**
	 * 描述：执行的动作
	 * 
	 * @author tch
	 */
	public enum Action {
		ADD("新增"), //
		UPDATE("修改"), //
		DELETE("删除");//
		/**
		 * 名称
		 */
		private String name;

		private Action(String name) {
			this.name = name;
		}

		/**
		 * @return the name
		 */
		public String getName() {
			return name;
		}

		/**
		 * @param name
		 *            the name to set
		 */
		public void setName(String name) {
			this.name = name;
		}

	}

	/**
	 * 条件And
	 * @param paraMap
	 * @return
	 * @throws SQLException
	 */
	@SuppressWarnings("unchecked")
	public List<Map<String, Object>> getUniqueDomain(Map<String, Object> paraMap) throws SQLException {
		return getSqlMap().queryForList("Common" + getStatementDivision() + "getUniqueBySql", paraMap);
	}
	
	/**
	 * 条件Or
	 * @param paraMap
	 * @return
	 * @throws SQLException
	 */
	@SuppressWarnings("unchecked")
	public List<Map<String, Object>> getUniqueBySqlConditionOr(Map<String, Object> paraMap) throws SQLException {
		return getSqlMap().queryForList("Common" + getStatementDivision() + "getUniqueBySqlConditionOr", paraMap);
	}

	/**
	 * 
	 * TODO 操作日志
	 * 
	 * @author Jul 5, 2012 2:32:57 PM ZQ
	 * @param domain
	 * @param action
	 * @throws Exception
	 * 
	 */
	private void writeOperation(M domain, Action action) throws Exception {
		if (domain instanceof IOperatingUnlogable) {// 实现不记录操作日志接口
			return;
		}
		HttpServletRequest request = null;
		try {
			request = ServletActionContext.getRequest();
		} catch (Exception e) {
		}
		HttpSession session = request == null ? null : request.getSession();
		// 登录用户信息
		User user = session == null ? null : (User) session.getAttribute(ComitConstant.USER_SESSION_ATTRIBUTE);
		if (null == user) {
			user = new User();
		}
		// IP地址
		String remoteAddr = request == null ? "非Web操作" : HTTPUtils.getRemoteAddr(request);
		// 服务器
		String hostAddr = request == null ? "非Web操作" : request.getServerName();
		// 端口
		String port = request == null ? "非Web操作" : String.valueOf(request.getServerPort());
		// 操作类型
		String act = action.getName();
		// 操作内容
		String actInfo = JSONUtils.object2JSON(domain);
		// 操作日志对象
		OperatingLog ol = new OperatingLog(user.getId(), new Date(), act, remoteAddr, hostAddr, port, domain.getClass().getName(), domain.getId(), actInfo);
		OperatingLogJob.addOperatingLog(ol);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.toncen.samepms.common.IDao#beginTransaction()
	 */
	public void beginTransaction() throws Exception {
		getSqlMap().startTransaction();

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.toncen.samepms.common.IDao#commitTransaction()
	 */
	public void commitTransaction() throws Exception {
		getSqlMap().commitTransaction();

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.toncen.samepms.common.IDao#rollback()
	 */
	public void rollback() throws Exception {
		clearCache();// 清除当前缓存
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.toncen.samepms.common.IDao#endTransaction()
	 */
	public void endTransaction() throws Exception {
		getSqlMap().endTransaction();

	}

	/**
	 * @return the statementDivision
	 */
	public String getStatementDivision() {
		return statementDivision;
	}

	/**
	 * @param statementDivision
	 *            the statementDivision to set
	 */
	public void setStatementDivision(String statementDivision) {
		this.statementDivision = statementDivision;
	}

	/**
	 * @return the statementPrefix
	 */
	public String getStatementPrefix() {
		return statementPrefix;
	}

	/**
	 * @param statementPrefix
	 *            the statementPrefix to set
	 */
	public void setStatementPrefix(String statementPrefix) {
		this.statementPrefix = statementPrefix;
	}

	/**
	 * 获取IBatis的SqlMapClient实例
	 * 
	 * @return the sqlMap
	 */
	protected SqlMapClient getSqlMap() {
		if (this.SqlMap == null) {// 没有加载，则加载配置文件
			try {
				this.SqlMap = getSqlMapInstance(configFile);
			} catch (Throwable e) {
				throw new IllegalArgumentException("加载SQLMAP配置文件错误:" + e.getMessage(), e);
			}
		}
		return this.SqlMap;
	}

	/**
	 * 初始化statement的前缀、分隔符号等等,子类一定要实现此方法
	 */
	protected void initialize() {

	}

	/**
	 * 获取statement的全名称，总共由三部分组成：Prefix(前缀)+Division(分隔符)+Postfix(后缀)
	 * 
	 * @param statementPostfix
	 * @return
	 */
	protected String getStatement(String statementPostfix) {
		return getStatementPrefix() + getStatementDivision() + statementPostfix;
	}

	/**
	 * 取得当前数据库实例,如果设置虚拟机的自定义参数devMode=true，则每次都重新加载配置文件(使用时配置jvm参数:-DdevMode=true)
	 * 
	 * @param configFile
	 *            String
	 * @return SqlMapClient
	 * @throws IOException
	 */
	protected SqlMapClient getSqlMapInstance(String configFile) throws IOException {
		if (!CACHE_SQLMAPCLIENTS.containsKey(configFile)) {
			logger.debug("开始加载配置文件【" + configFile + "】...");
			Resources.setCharset(Charset.forName("UTF-8"));
			Reader reader = Resources.getResourceAsReader(configFile);
			SqlMapClient sqlMapper = (SqlMapClient) SqlMapClientBuilder.buildSqlMapClient(reader);
			reader.close();
			CACHE_SQLMAPCLIENTS.put(configFile, sqlMapper);
		}
		return CACHE_SQLMAPCLIENTS.get(configFile);
	}
}
